package dk.aiszone.lang.sql;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import dk.aiszone.lang.sql.SQLResult.UnknownIdException;
import dk.aiszone.lang.sql.interfaces.SQLConstraint;
import dk.aiszone.lang.sql.interfaces.SQLFieldConstraintObject;
import dk.aiszone.lang.sql.interfaces.SQLJoin;
import dk.aiszone.lang.sql.interfaces.SQLPart;

/**
 * Representation of the <i>Select</i> SQL statement.
 * 
 * @author Ais
 */
public class SQLSelect extends AbstractSQLObject implements SQLFieldConstraintObject {

    private SQLAnd basisAndstatement;
    private SQLAnd currentAndStatement;
    private boolean distinct;
    private List<SQLJoin> joins = new ArrayList<SQLJoin>();
    private SQLResult prepared = null;
    private SQLTable table = null;

    /**
     * Constructor for the <code>SQLSelect</code> class.
     * 
     * @param table
     *            <code>SQLTable</code> to select.
     */
    public SQLSelect(SQLTable table) {
        this.table = table;
    }

    public void addConstraint(SQLConstraint constraint) {
        assert prepared == null : "Statement already prepared";

        if (currentAndStatement == null) {
            currentAndStatement = new SQLAnd(constraint);
            basisAndstatement = currentAndStatement;
        } else {
            SQLAnd tmp = new SQLAnd(constraint);
            currentAndStatement.setConstraint2(tmp);
            currentAndStatement = tmp;
        }
    }

    public void addConstraint(SQLOr or) {
        assert prepared == null : "Statement already prepared";

        if (currentAndStatement == null) {
            currentAndStatement = new SQLAnd(or);
            basisAndstatement = currentAndStatement;
        } else {
            SQLAnd tmp = new SQLAnd(or);
            currentAndStatement.setConstraint2(tmp);
            currentAndStatement = tmp;
        }
    }

    public void addField(SQLField field) {
        assert prepared == null : "Statement already prepared";

        table.addField(field);
    }

    public void addJoin(SQLJoin join) {
        assert prepared == null : "Statement already prepared";

        joins.add(join);
    }

    /**
     * Retrieve the <code>SQLTable</code> of the <code>SQLSelec</code>.
     * 
     * @return The selected table.
     */
    public SQLTable getTable() {
        return table;
    }

    /**
     * Set whether the <b>DISTINCT</b> keyword should be appended to the SQL statement.
     * 
     * @param distinct
     *            <code>true</code> to append - otherwise <code>false</code>.
     */
    public void setDistinct(boolean distinct) {
        assert prepared == null : "Statement already prepared";

        this.distinct = distinct;
    }

    /**
     * Set the value of the assignable constraint with the specified id.
     * 
     * @param id
     *            The id of the assignable constraint to assign value to.
     * @param value
     *            The value to assign.
     * @throws UnknownIdException
     *             Thrown if the specified id is unknown.
     */
    public void setValue(String id, Object value) throws UnknownIdException {
        if (prepared == null) {
            prepare();
        }

        prepared.setValue(id, value);
    }

    /**
     * Convert the <code>SQLSelect</code> to a prepared statement <code>String</code>.
     * 
     * @return The prepared statement <code>String</code> representation of the <code>SQLSelect</code> object.
     */
    public String toPreparedStatement() {
        if (prepared == null) {
            prepare();
        }

        return prepared.toPreparedStatement();
    }

    /**
     * Convert the <code>SQLSelect</code> to a SQL <code>String</code>.
     * 
     * @return The <code>String</code> representation of the <code>SQLSelect</code> object.
     */
    public String toSQL() {
        if (prepared == null) {
            prepare();
        }

        return prepared.toSQL();
    }

    private Set<SQLField> collectAllFields() {
        Set<SQLField> fields = new LinkedHashSet<SQLField>();
        for (Iterator<SQLField> fieldIterator = table.getFields().iterator(); fieldIterator.hasNext();) {
            fields.add(fieldIterator.next());
        }
        for (Iterator<SQLJoin> joinIterator = joins.iterator(); joinIterator.hasNext();) {
            for (Iterator<SQLField> fieldIterator = joinIterator.next().getJoinedTable().getFields().iterator(); fieldIterator.hasNext();) {
                fields.add(fieldIterator.next());
            }
        }
        return fields;
    }

    /**
     * Prepares the SQL select statement.<br>
     */
    private void prepare() {
        prepared = new SQLResult();

        Set<SQLField> fields = collectAllFields();

        prepared.append("SELECT ");
        if (distinct) {
            prepared.append("DISTINCT ");
        }
        for (Iterator<SQLField> fieldIterator = fields.iterator(); fieldIterator.hasNext();) {
            fieldIterator.next().prepareSQL(prepared);
            if (fieldIterator.hasNext()) {
                prepared.append(", ");
            }
        }

        prepared.append(" FROM ");
        table.prepareSQL(prepared);

        for (Iterator<SQLJoin> joinIterator = joins.iterator(); joinIterator.hasNext();) {
            joinIterator.next().prepareSQL(prepared);
        }

        if (basisAndstatement != null) {
            prepared.append(new Dependecy(basisAndstatement));
            basisAndstatement.prepareSQL(prepared);
        }
    }

    private static class Dependecy implements SQLPart {

        private final SQLAnd dependency;

        /* pp */Dependecy(SQLAnd dependency) {
            this.dependency = dependency;
        }

        @Override
        public String toPreparedStatement() {
            return toSQL();
        }

        @Override
        public String toSQL() {
            if (dependency.isAssigned()) {
                return " WHERE ";
            }
            return "";
        }
    }
}